昨天我完成了 Express + Prisma + TypeScript 的整合,
今天的目標是把錯誤處理「集中管理」,
並調整專案架構,讓控制器、服務、DTO 各自負責,
程式變得更乾淨、更可維護。
目前錯誤處理是分散在各個路由裡,例如:
try {
await prisma.todo.delete({ where: { id } });
} catch {
res.status(404).json({ error: "找不到這筆 Todo" });
}
這樣每個 API 都要重複 try/catch
。
如果未來錯誤邏輯要修改,就得改一堆地方。
我們要做的三件事:
HttpException
)src/
├── controllers/
│ └── todo.controller.ts
├── services/
│ └── todo.service.ts
├── middleware/
│ ├── error-handler.ts
│ └── validate-dto.ts
├── dto/
│ ├── create-todo.dto.ts
│ └── update-todo.dto.ts
├── prisma/
│ └── client.ts
├── routes/
│ └── todo.routes.ts
├── utils/
│ └── http-exception.ts
├── index.ts
src/utils/http-exception.ts
export class HttpException extends Error {
status: number;
message: string;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.message = message;
}
}
這樣我就能用 throw new HttpException(404, "找不到這筆資料")
來拋出錯誤。
src/middleware/error-handler.ts
import { Request, Response, NextFunction } from "express";
import { HttpException } from "../utils/http-exception";
export function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction) {
console.error("❌ 錯誤發生:", err);
if (err instanceof HttpException) {
return res.status(err.status).json({ error: err.message });
}
// 非預期錯誤
return res.status(500).json({ error: "伺服器內部錯誤" });
}
這樣所有 throw new HttpException()
的錯誤都會被自動捕捉。
src/services/todo.service.ts
import prisma from "../prisma/client";
import { HttpException } from "../utils/http-exception";
export class TodoService {
async findAll() {
return prisma.todo.findMany();
}
async create(task: string, note?: string) {
return prisma.todo.create({ data: { task, note } });
}
async update(id: number, data: any) {
const todo = await prisma.todo.findUnique({ where: { id } });
if (!todo) throw new HttpException(404, "找不到這筆 Todo");
return prisma.todo.update({ where: { id }, data });
}
async delete(id: number) {
const todo = await prisma.todo.findUnique({ where: { id } });
if (!todo) throw new HttpException(404, "找不到這筆 Todo");
await prisma.todo.delete({ where: { id } });
return { message: "Todo 已刪除" };
}
}
Service 負責邏輯,不管輸入輸出,只專心操作資料。
src/controllers/todo.controller.ts
import { Request, Response, NextFunction } from "express";
import { TodoService } from "../services/todo.service";
const todoService = new TodoService();
export class TodoController {
async getAll(req: Request, res: Response, next: NextFunction) {
try {
const todos = await todoService.findAll();
res.json(todos);
} catch (err) {
next(err);
}
}
async create(req: Request, res: Response, next: NextFunction) {
try {
const { task, note } = req.body;
const todo = await todoService.create(task, note);
res.status(201).json(todo);
} catch (err) {
next(err);
}
}
async update(req: Request, res: Response, next: NextFunction) {
try {
const id = Number(req.params.id);
const todo = await todoService.update(id, req.body);
res.json(todo);
} catch (err) {
next(err);
}
}
async delete(req: Request, res: Response, next: NextFunction) {
try {
const id = Number(req.params.id);
const result = await todoService.delete(id);
res.json(result);
} catch (err) {
next(err);
}
}
}
Controller 專心處理 請求與回應,不碰業務邏輯。
src/routes/todo.routes.ts
import { Router } from "express";
import { validateDto } from "../middleware/validate-dto";
import { CreateTodoDto } from "../dto/create-todo.dto";
import { UpdateTodoDto } from "../dto/update-todo.dto";
import { TodoController } from "../controllers/todo.controller";
const router = Router();
const controller = new TodoController();
router.get("/", controller.getAll.bind(controller));
router.post("/", validateDto(CreateTodoDto), controller.create.bind(controller));
router.put("/:id", validateDto(UpdateTodoDto), controller.update.bind(controller));
router.delete("/:id", controller.delete.bind(controller));
export default router;
src/index.ts
import "reflect-metadata";
import express from "express";
import todoRoutes from "./routes/todo.routes";
import { errorHandler } from "./middleware/error-handler";
const app = express();
app.use(express.json());
app.use("/todos", todoRoutes);
app.use(errorHandler); // 全域錯誤處理要放在最後
app.listen(3000, () => {
console.log("🚀 Server running on http://localhost:3000");
});
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"task": "Day 26 完成錯誤處理"}'
curl -X PUT http://localhost:3000/todos/999 \
-H "Content-Type: application/json" \
-d '{"task": "不存在的 Todo"}'
輸出:
{ "error": "找不到這筆 Todo" }
🎯 成功!錯誤被集中處理、訊息乾淨又一致。
今天讓整個 API 架構升級成「真正可維護」的版本:
HttpException
讓錯誤更語意化errorHandler
中央化錯誤管理以前寫 Express 是「能跑就好」,
現在這樣的結構,已經接近真正的專案規模。